<?php

namespace App\Kuafu\Pay\Service;

use App\Kuafu\Activity\Enum\ActivityEnum;
use App\Kuafu\Base\Service\BaseService;
use App\Kuafu\CouponExt\Channel\CouponExtBase;
use App\Kuafu\CouponExt\Enum\CouponExtCodeEnum;
use App\Kuafu\CouponExt\Enum\CouponExtEnum;
use App\Kuafu\CouponExt\Exceptions\CouponExtException;
use App\Kuafu\CouponExt\Model\OrderCouponExtSplitModel;
use App\Kuafu\CouponPackage\Model\CouponPackageModel;
use App\Kuafu\CouponPackage\Model\CouponPackageOrderModel;
use App\Kuafu\CustomerCoupon\Exceptions\CustomerCouponException;
use App\Kuafu\CustomerCoupon\Service\CustomerCouponService;
use App\Kuafu\CustomPayment\Enum\CustomPaymentEnum;
use App\Kuafu\Mybank\Service\MybankCashierOrderService;
use App\Kuafu\Nsq\Enum\NsqCodeEnum;
use App\Kuafu\Order\Enum\CodeEnum;
use App\Kuafu\Order\Exceptions\OrderException;
use App\Kuafu\Order\Model\OrderSplitModel;
use App\Kuafu\Order\Service\OrderPay;
use App\Kuafu\Pay\BasePay;
use App\Kuafu\Pay\Enum\BasePayEnum;
use App\Kuafu\Pay\Enum\FuYouPayEnum;
use App\Kuafu\Pay\Enum\PayCacheEnum;
use App\Kuafu\Pay\Enum\PayCodeEnum;
use App\Kuafu\Pay\Exceptions\PayException;
use App\Kuafu\Pay\Jobs\PayPolling;
use App\Kuafu\Pay\Jobs\PayRefundCallback;
use App\Kuafu\Pay\Jobs\RefundPolling;
use App\Kuafu\Pay\Model\PayCallbackLog;
use App\Kuafu\Pay\Model\PayStreamModel;
use App\Kuafu\Pay\Model\RefundStreamModel;
use App\Kuafu\Shop\Enum\ShopCodeEnum;
use App\Kuafu\Shop\Exceptions\ShopException;
use App\Kuafu\User\Model\User;
use App\Library\FeiShu\SendFs;
use App\Library\SnowFlake\SnowFlake;
use GuzzleHttp\Client;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;
use Kuafu\Nsq\NsqPublisher;

class PayService extends BaseService
{
    /**
     * 用户被扫 - 付款码支付
     * @param int $shopId
     * @param int $orderId
     * @param int $payPrice
     * @param string $orderDesc
     * @param string $authCode
     * @return array
     * @throws \App\Kuafu\Pay\Exceptions\PayException
     */
    public static function sFaceToFacePay(
        int $shopId,
        int $orderId,
        int $payPrice,
        string $orderDesc,
        string $authCode,
        int $sysType
    ): array {

        $payChannel = PayStreamModel::PAY_CHANNEL_FY;

        // 自动判断payChildChannel
        $payChannelChild = BasePay::checkAuthCode($authCode);

        // 生成我方的支付流水
        $payNo = BasePay::instance($payChannel, $shopId, $sysType)->getOutTradeNo();

        list($fee, $feeRate) = BasePay::instance($payChannel, $shopId, $sysType)
                                      ->setChildPayChannel($payChannelChild)
                                      ->getFeeAmtByPayPrice($payPrice);

        // 支付先入库
        $bizId = (string)$orderId;
        PayStreamModel::addData($payNo, $shopId, $orderId, $payChannel, $payChannelChild,
            PayStreamModel::PAY_ACTION_QRCODE, $payPrice, $fee, $feeRate, $sysType, '', 1, $orderDesc, $bizId);

        $pay = BasePay::instance($payChannel, $shopId, $sysType)->faceToFacePay($orderDesc, $payPrice, $authCode);

        if (!$pay['status']) {

            // 需要用户输入密码
            if ($pay['data']['tradeStatus'] == BasePayEnum::TRADE_STATUS_WAIT) {

                $thirdPayNo = $pay['data']['thirdPayNo'] ?? '';
                if ($thirdPayNo) {
                    PayStreamModel::saveThirdPaySerialNo($payNo, $thirdPayNo, $shopId, $sysType);
                }

                // 生成队列，轮询查询支付结果
                PayPolling::dispatch($payNo)->onQueue(PayPolling::QUEUE_NAME);

                $payStatus = BasePayEnum::TRADE_STATUS_WAIT;
                return [
                    'payStatus'  => $payStatus,
                    'payChannel' => $payChannel,
                    'payAction'  => PayStreamModel::PAY_ACTION_QRCODE,
                    'payNo'      => $payNo
                ];
            } else {

                // 支付失败
                throw new PayException(PayCodeEnum::ERR_PAY_FACE_TO_FACE_PAY, $pay['msg']);
            }
        }

        // 支付成功 - 更新DB
        PayStreamModel::paySuccess($payNo, $pay['data']['paySuccessTime'], $pay['data']['thirdPayNo']);

        $payStatus = BasePayEnum::TRADE_STATUS_OK;

        return [
            'payStatus'  => $payStatus,
            'payChannel' => $payChannel,
            'payAction'  => PayStreamModel::PAY_ACTION_QRCODE,
            'payNo'      => $payNo
        ];
    }

    /**
     * 现金支付
     * @param int $shopId
     * @param int $orderId
     * @param int $payPrice
     * @return array
     * @throws PayException
     */
    public static function cashPay(
        int $shopId,
        int $orderId,
        int $payPrice,
        int $sysType = PayStreamModel::SYS_TYPE_RMS_MEAL
    ): array {
        // 生成我方的支付流水
        $payChannel      = PayStreamModel::PAY_CHANNEL_CASH;
        $payChannelChild = PayStreamModel::PAY_CHANNEL_CHILD_NULL;
        $payAction       = PayStreamModel::PAY_ACTION_NULL;
        $thirdNo         = $payNo = BasePay::instance($payChannel, $shopId, 0)->getOutTradeNo(true);
        $paySuccessTime  = time();

        list($fee, $feeRate) = BasePay::instance($payChannel, $shopId, 0)
                                      ->setChildPayChannel($payChannelChild)
                                      ->getFeeAmtByPayPrice($payPrice);

        $bizId = (string)$orderId;
        PayStreamModel::addData($payNo, $shopId, $orderId, $payChannel, $payChannelChild, $payAction, $payPrice, $fee,
            $feeRate, $sysType, '', 1, '', $bizId);
        PayStreamModel::paySuccess($payNo, $paySuccessTime, $thirdNo);

        return [
            'payStatus'  => BasePayEnum::TRADE_STATUS_OK,
            'payChannel' => $payChannel,
            'payAction'  => $payAction,
            'payNo'      => $payNo
        ];
    }

    /**
     * 自定义支付
     *
     * @param int $shopId
     * @param int $orderId
     * @param int $payPrice
     * @return array
     * @throws PayException
     */
    public static function customPay(int $shopId, int $orderId, int $payPrice, int $payChannel, int $reckonType): array
    {
        // 生成我方的支付流水
        $payChannelChild = PayStreamModel::PAY_CHANNEL_CHILD_NULL;
        $payAction       = PayStreamModel::PAY_ACTION_NULL;
        $thirdNo         = $payNo = BasePay::instance($payChannel, $shopId, 0)->getOutTradeNo(true);
        $paySuccessTime  = time();


        list($fee, $feeRate) = BasePay::instance($payChannel, $shopId, 0)
                                      ->setChildPayChannel($payChannelChild)
                                      ->getFeeAmtByPayPrice($payPrice);

        $bizId = (string)$orderId;
        PayStreamModel::addData($payNo, $shopId, $orderId, $payChannel, $payChannelChild, $payAction, $payPrice, $fee,
            $feeRate, PayStreamModel::SYS_TYPE_RMS_MEAL, '',
            $reckonType == CustomPaymentEnum::RECKON_TYPE_INCOME ? 1 : 0, '', $bizId);
        PayStreamModel::paySuccess($payNo, $paySuccessTime, $thirdNo);

        return [
            'payStatus'  => BasePayEnum::TRADE_STATUS_OK,
            'payChannel' => $payChannel,
            'payAction'  => $payAction,
            'payNo'      => $payNo
        ];
    }

    /**
     * 单笔支付查询
     * @param string $payNo
     * @throws PayException
     */
    public static function sQuery(string $payNo)
    {
        $payStream = PayStreamModel::getDataByPayNo($payNo);
        if (!$payStream) {
            throw new PayException(PayCodeEnum::ERR_PAY_DATA_NOT_EXIST);
        }

        return $payStream;
    }

    /**
     * 强制刷新支付结果
     * @param string $orderId
     * @param int $shopId
     * @param int $sysType
     * @return array
     * @throws CustomerCouponException
     * @throws OrderException
     * @throws PayException
     */
    public static function sRefreshQuery(string $orderId, int $shopId, int $sysType): array
    {
        $payStream = PayStreamModel::suffix($shopId, $sysType)
                                   ->where('shopId', $shopId)
                                   ->where('sysType', $sysType)
                                   ->where('bizId', $orderId)
                                   ->where('payChannel', PayStreamModel::PAY_CHANNEL_FY)
                                   ->orderByDesc('createdAt')
                                   ->first();

        if (!$payStream) {
            throw new PayException(PayCodeEnum::ERR_PAY_DATA_NOT_EXIST);
        }

        if ($payStream->payStatus == PayStreamModel::PAY_STATUS_1) {
            $payStatus = $payStream->payStatus;
        } else {
            list($_, $payStatus) = self::sPayPolling($payStream);
        }

        return [
            'payNo'     => $payStream->payNo,
            'orderId'   => $payStream->orderId,
            'payStatus' => $payStatus,
        ];
    }

    /**
     * 支付结果操作-原子锁
     * @param string $payNo
     * @return bool
     */
    private static function payResultOperateLock(string $payNo): bool
    {
        // 加锁
        $redis_key     = PayCacheEnum::getPayResultOperateLock($payNo);
        $expireSeconds = 5;

        if (!Redis::setnx($redis_key, time())) {

            // 防止死锁
            $lock_time = Redis::get($redis_key);
            if (($lock_time + $expireSeconds) < time()) {
                Redis::del($redis_key);
            } else {

                // 未获取到锁
                Log::warning("payResultOperateLock jam key: {$redis_key}");
                return false;
            }
        }

        // get
        return true;
    }

    /**
     * 主动查询支付结果，并更新
     * @param PayStreamModel|null $payStream
     * @return string[]
     * @throws CustomerCouponException
     * @throws OrderException
     * @throws PayException
     */
    public static function sPayPolling(?PayStreamModel $payStream): array
    {
        $do = function () use ($payStream) {

            if (!$payStream) {
                return ['delete', -1];
            }

            $payNo = $payStream->payNo;

            if (!self::payResultOperateLock($payNo)) {
                return ['release', PayStreamModel::PAY_STATUS_0];
            }

            // 查三方支付信息
            $payQuery = BasePay::instance($payStream->payChannel, $payStream->shopId, $payStream->sysType, $payStream->company)
                               ->query($payNo);
            if (!$payQuery['status']) {

                // 查询失败，先延迟再说
                return ['release', PayStreamModel::PAY_STATUS_0];
            }

            $payStatus = $payQuery['data']['commonTradeStatus'] ?? BasePayEnum::TRADE_STATUS_WAIT;

            // 未付款，延迟
            if ($payStatus == BasePayEnum::TRADE_STATUS_WAIT) {
                return ['release', PayStreamModel::PAY_STATUS_0];
            }

            // 付款成功
            if ($payStatus == BasePayEnum::TRADE_STATUS_OK) {

                // 更新成功状态
                PayStreamModel::paySuccess(
                    $payNo,
                    $payQuery['data']['paySuccessTime'],
                    $payQuery['data']['thirdPayNo']
                );

                // 支付成功要干的业务操作
                PayService::businessAsynPaySuccess($payStream);

                return ['delete', PayStreamModel::PAY_STATUS_1];
            }

            // 交易关闭
            if ($payStatus == BasePayEnum::TRADE_STATUS_CLOSED) {

                // 已经退款了，支付表里标记成成功
                if ($payQuery['data']['paySuccessTime']) {

                    // 更新成功状态
                    PayStreamModel::paySuccess(
                        $payNo,
                        $payQuery['data']['paySuccessTime'],
                        $payQuery['data']['thirdPayNo']
                    );
                } else {
                    // 交易关闭
                    Log::warning("PayPolling payNo:{$payNo}交易关闭", $payQuery);
                }

                return ['delete', -1];
            }

            // 关闭支付
            return ['delete', -1];
        };

        $res = $do();

        Redis::del(PayCacheEnum::getPayResultOperateLock($payStream->payNo));

        return $res;
    }

    /**
     * 退款
     * @param int $shopId
     * @param int $orderId
     * @param int $refundPrice 退款金额
     * @return array
     * @throws PayException
     */
    public static function sRefund(
        int $shopId,
        int $orderId,
        int $refundPrice,
        string $callbackUrl = '',
        string $payNo = '',
        string $subject = ''
    ): array {
        // 查原始订单的支付记录
        $payStreams = PayStreamModel::getPayRefundsByOrderId($orderId, $payNo, [], PayStreamModel::PAID_IN_ALL,
            $shopId);
        if ($payStreams->count() == 0) {
            // 支付数据不存在
            throw new PayException(PayCodeEnum::ERR_PAY_DATA_NOT_EXIST);
        }

        // 已支付金额
        $totalPay = $payStreams->sum('payPrice');

        // 已赎回金额
        $hasRefundMoney = 0;
        foreach ($payStreams as $pay) {
            if ($pay->refundStreamRel->count()) {
                $hasRefundMoney += $pay->refundStreamRel->sum('refundPrice');
            }
        }

        // 剩余金额不够赎回
        if ($totalPay - $hasRefundMoney - $refundPrice < 0) {
            throw new PayException(PayCodeEnum::ERR_PAY_REFUND_NOT_ENOUGH);
        }

        // 退单条的方法
        $refundOne = function ($payStream, $refundPrice) use ($shopId, $orderId, $callbackUrl, $subject) {

            // 退款渠道采用原支付渠道
            $refundChannel      = $payStream->payChannel;
            $refundChannelChild = $payStream->payChannelChild;
            $payNo              = $payStream->payNo;
            $refundType         = RefundStreamModel::REFUND_TYPE_PART;
            $sysType            = $payStream->sysType;

            // 生成我方的退款流水
            $refundNo = BasePay::instance($refundChannel, $shopId, $sysType)->getOutTradeNo(true);

            RefundStreamModel::addData(
                $refundNo,
                $shopId,
                $orderId,
                $payNo,
                $refundChannel,
                $refundChannelChild,
                $refundType,
                $refundPrice,
                $sysType,
                $subject
            );

            // 现金支付/自定义支付 - 直接返回成功
            if ($refundChannel == PayStreamModel::PAY_CHANNEL_CASH || $refundChannel >= PayStreamModel::PAY_CHANNEL_CUSTOM_PAYMENT) {
                RefundStreamModel::refundSuccess($refundNo, time(), $refundNo);
                $payStatus = BasePayEnum::REFUND_STATUS_OK;

                return [
                    'refundStatus'  => $payStatus,
                    'refundChannel' => $payStream->payChannelChild,
                    'payNo'         => $payNo,
                    'refundNo'      => $refundNo,
                    'refundPrice'   => $refundPrice,
                ];
            }

            // 发起退款
            $refund = BasePay::instance($refundChannel, $shopId, $sysType)->refund($payNo, $refundPrice);

            if (isset($refund['data']['thirdRefundNo']) && $refund['data']['thirdRefundNo']) {
                RefundStreamModel::query()->where('refundNo', $refundNo)
                                 ->update(['thirdRefundNo' => $refund['data']['thirdRefundNo']]);
            }

            // 查退款信息
            $refundQuery  = BasePay::instance($refundChannel, $shopId, $sysType)->refundQuery($payNo, $refundNo);
            $refundStatus = $refundQuery['data']['refundStatus'] ?? BasePayEnum::REFUND_STATUS_WAIT;

            // 都失败就失败了呗
            if ((!$refund['status'] && !$refundQuery['status']) || $refundStatus == BasePayEnum::REFUND_STATUS_ERROR) {

                // 置为失败
                RefundStreamModel::refundFail($refundNo);

                // 己方的内部服务回调
                PayService::paySuccessCallback($callbackUrl, PayStreamModel::OPERATE_TYPE_REFUND, $orderId, $refundNo);

                // 支付失败
                throw new PayException(PayCodeEnum::ERR_REFUND_ERR, $refund['msg']);
            }

            // 退款成功更新
            if ($refundQuery['data']['refundStatus'] == BasePayEnum::REFUND_STATUS_OK) {

                RefundStreamModel::refundSuccess($refundNo, $refundQuery['data']['refundSuccessTime'],
                    $refundQuery['data']['thirdRefundNo']);

                $payStatus = BasePayEnum::REFUND_STATUS_OK;

                if ($callbackUrl) {

                    // 己方的内部服务回调
                    self::paySuccessCallback($callbackUrl, PayStreamModel::OPERATE_TYPE_REFUND, $orderId, $refundNo);
                }

            } else {

                // 退款处理中
                $payStatus = BasePayEnum::REFUND_STATUS_WAIT;

                // 生成队列，轮询查询退款结果
                RefundPolling::dispatch($refundNo, $payNo)->onQueue(RefundPolling::QUEUE_NAME);
            }

            return [
                'refundStatus'  => $payStatus,
                'refundChannel' => $payStream->payChannelChild,
                'payNo'         => $payNo,
                'refundNo'      => $refundNo,
                'refundPrice'   => $refundPrice,
            ];
        };

        $res = [];

        foreach ($payStreams as $payStream) {

            // 当前支付订单的可赎回金额
            $oneRefundPrice = $payStream->payPrice - $payStream->refundStreamRel->sum('refundPrice');
            if ($oneRefundPrice == 0) {
                continue;
            }

            // 够扣了，退部分
            if ($refundPrice < $oneRefundPrice) {
                $oneRefundPrice = $refundPrice;
            }

            $res[] = $refundOne($payStream, $oneRefundPrice);

            $refundPrice -= $oneRefundPrice;
            if ($refundPrice <= 0) {
                break;
            }
        }

        return $res;
    }

    /**
     * 小程序支付
     * @param int $shopId
     * @param int $orderId
     * @param string $orderDesc
     * @param int $payPrice
     * @param string $openId
     * @return string[]
     * @throws PayException
     */
    public static function sMiniPay(
        int $shopId,
        int $orderId,
        int $payPrice,
        string $orderDesc,
        string $openId,
        int $sysType = PayStreamModel::SYS_TYPE_RMS_MEAL
    ) {
        // 校验orderId是否已支付，目前仅支持一笔订单一笔支付
        if (PayStreamModel::suffix($shopId, $sysType)->where('orderId', $orderId)
                          ->where('payStatus', PayStreamModel::PAY_STATUS_1)->count()) {
            throw new PayException(PayCodeEnum::ERR_ORDER_HAS_PAY);
        }

        $payChannel      = PayStreamModel::PAY_CHANNEL_FY;
        $payChannelChild = PayStreamModel::PAY_CHANNEL_WX;

        // 生成我方的支付流水
        $payNo = BasePay::instance($payChannel, $shopId, $sysType)->getOutTradeNo();

        list($fee, $feeRate) = BasePay::instance($payChannel, $shopId, $sysType)
                                      ->setChildPayChannel($payChannelChild)
                                      ->getFeeAmtByPayPrice($payPrice);

        // 支付先入库
        $bizId = (string)$orderId;
        PayStreamModel::addData($payNo, $shopId, $orderId, $payChannel, $payChannelChild,
            PayStreamModel::PAY_ACTION_MINI, $payPrice, $fee, $feeRate, $sysType, '', 1, '', $bizId);

        $pay = BasePay::instance($payChannel, $shopId, $sysType)->miniPay($orderDesc, $payPrice, $openId);
        if (!$pay['status']) {
            throw new PayException(PayCodeEnum::ERR_MINI_PREPAY_ERROR, $pay['msg']);
        }

        // 生成队列，轮询查询支付结果
//        PayPolling::dispatch($payNo)->onQueue(PayPolling::QUEUE_NAME);

        /*
        [
            "appId"     => "",
            "timeStamp" => "",
            "nonceStr"  => "",
            "package"   => "",
            "signType"  => "",
            "payNo"     => "",
        ];
        */

        return $pay['data'];
    }

    public static $callBackId = 0;

    /**
     * 支付回调
     * @param int $payChannel
     * @param array $requestData
     * @return bool
     * @throws PayException
     * @throws OrderException
     */
    public static function sPayCallBackLog(
        int $payChannel,
        array $requestData,
        int $sysType = PayStreamModel::SYS_TYPE_ROS,
        int $company = 0
    ): bool {
        // 通过payNo，获取payStream，不存在是报错
        $getPayStream = function (string $payNo) {
            $payStream = PayStreamModel::getDataByPayNo($payNo);
            if (!$payStream) {
                throw new PayException(PayCodeEnum::ERR_PAY_DATA_NOT_EXIST);
            }

            return $payStream;
        };

        switch ($payChannel) {
            case PayStreamModel::PAY_CHANNEL_FY:
                $serialNo         = $requestData['mchnt_order_no'] ?? '';
                self::$callBackId = PayCallbackLog::addData($payChannel, $serialNo, json_encode($requestData));

                $payStream = $getPayStream($serialNo);
                $pay       = BasePay::instance($payChannel, $payStream->shopId, $payStream->sysType, $payStream->company)
                                    ->query($serialNo);

                break;
            case PayStreamModel::PAY_CHANNEL_ALI:
                $serialNo         = $requestData['out_trade_no'] ?? '';
                self::$callBackId = PayCallbackLog::addData($payChannel, $serialNo, json_encode($requestData));

                // 这是退款的回调 - 先不处理
                if (isset($requestData['refund_fee']) && $requestData['refund_fee'] > 0) {
                    return false;
                }

                $payStream = $getPayStream($serialNo);

                $pay = BasePay::instance($payChannel, 0, $payStream->sysType, $payStream->company)
                              ->decryptAppCallback($requestData);
                break;
            case PayStreamModel::PAY_CHANNEL_WX:
                $pay              = BasePay::instance($payChannel, 0, $sysType, $company)
                                           ->decryptAppCallback($requestData);
                $serialNo         = $pay['data']['payNo'];
                self::$callBackId = PayCallbackLog::addData($payChannel, $serialNo, json_encode($requestData));

                $payStream = $getPayStream($serialNo);
                break;
            case PayStreamModel::PAY_CHANNEL_MYBANK_PAY:
            case PayStreamModel::PAY_CHANNEL_MYBANK_CASHIER_PAY:

                $serialNo  = $requestData['payNo'];
                $payStream = $getPayStream($requestData['payNo']);
                $pay       = BasePay::instance($payChannel, $payStream->shopId, $payStream->sysType, $payStream->company)
                                    ->query($requestData['payNo']);

                break;
            default:
                throw new PayException(PayCodeEnum::ERR_PAY_ERROR_PAY_CHANNEL);
        }

        // 加锁
        if (!self::payResultOperateLock($payStream->payNo)) {
            return false;
        }

        $tradeStatus = $pay['data']['commonTradeStatus'];

        // 支付成功
        if ($tradeStatus == BasePayEnum::TRADE_STATUS_OK) {
            $thirdPayNo     = $pay['data']['thirdPayNo'] ?? '';
            $paySuccessTime = $pay['data']['paySuccessTime'] ?? 0;

            $updatePayStatus = PayStreamModel::paySuccess($serialNo, $paySuccessTime, $thirdPayNo);
            if ($updatePayStatus) {

                // 支付成功要干的业务操作
                PayService::businessAsynPaySuccess($payStream);
            }

            Redis::del(PayCacheEnum::getPayResultOperateLock($payStream->payNo));

            return true;
        }

        throw new PayException(PayCodeEnum::ERR_QUERY_PAY_NOT_SUCCESS);
    }

    // 己方内部服务的订单支付、退款回调
    public static function paySuccessCallback(string $url, int $operateType, string $orderId, string $payRefundNo): void
    {
        // nsq publish
        if ($operateType == PayStreamModel::OPERATE_TYPE_PAY) {
            $pay = PayStreamModel::getDataByPayNo($payRefundNo);

            PayService::nsqPublishPaySuccess($pay);
        }

        // 目前仅推送支付成功
        if ($operateType != PayStreamModel::OPERATE_TYPE_PAY) {
            return;
        }

        if (!$url) {
            return;
        }

        // 回调
        if (config('app.env') == 'pre') {

            // pre没有队列，直接同步回调
            PayRefundCallback::dispatch($url, $operateType, $orderId, $payRefundNo)
                             ->onQueue(PayRefundCallback::QUEUE_NAME)->onConnection('sync');
        } else {
            PayRefundCallback::dispatch($url, $operateType, $orderId, $payRefundNo)
                             ->onQueue(PayRefundCallback::QUEUE_NAME);
        }
    }

    /**
     * 支付成功nsq publish
     * @param PayStreamModel $pay
     * @return void
     * @throws \Exception
     */
    public static function nsqPublishPaySuccess(PayStreamModel $pay)
    {
        $userId = $pay->createdByUid;
        $user   = $userId ? User::query()->find($userId) : [];
        if ($user) {
            $user = $user->toArray();
        }

        $param = [
            'orderId'    => $pay->orderId,
            'amount'     => $pay->payPrice,
            'payNo'      => $pay->payNo,
            'bizId'      => $pay->bizId,
            'payChannel' => $pay->payChannel,
            'thirdPayNo' => $pay->thirdPayNo,
            'sysType'    => $pay->sysType,
            'shopId'     => $pay->shopId,
            'userInfo'   => $user,
        ];

        // nsq pub支付成功
        (new NsqPublisher(config('nsq.default.address')))->pub(NsqCodeEnum::PAY_SUCCESS, $param);
    }

    /**
     * 检查couponCode是否可以反核销
     * @param int $shopId
     * @param string $couponCode
     * @return bool
     * @throws \GuzzleHttp\Exception\GuzzleException
     */
    public static function sCheckCancelCoupon(int $shopId, string $couponCode): bool
    {
        $o = CouponExtBase::getIns($shopId)->couponQuery($couponCode);
        if ($o['status'] && $o['data']['cancelStatus'] == CouponExtEnum::COUPON_CANCEL_STATUS_1) {
            return true;
        }

        return false;
    }

    /**
     * app支付
     * @param int $shopId
     * @param string $subject
     * @param int $amount
     * @param string $orderId
     * @param int $payChannel
     * @param int $payChannelChild
     * @param int $sysType
     * @param string $callbackUrl
     * @param string $bizId
     * @param int $expireTime
     * @param int $userId
     * @param int $payeeConfigId
     * @return mixed
     * @throws PayException
     */
    public static function sAppPay(
        int $shopId,
        string $subject,
        int $amount,
        string $orderId,
        int $payChannel,
        int $payChannelChild,
        int $sysType,
        string $callbackUrl,
        string $bizId,
        int $expireTime,
        int $userId = 0,
        int $payeeConfigId = 0
    ) {
        $pay = BasePay::instance($payChannel, $shopId, $sysType, $payeeConfigId)
                      ->setUserId($userId)
                      ->appPay($subject, $amount, $expireTime);
        if (!$pay['status']) {
            throw new PayException(PayCodeEnum::ERR_APP_PREPAY_ERROR, $pay['msg'] ?? '');
        }

        $payNo      = $pay['data']['payNo'];
        $thirdPayNo = $pay['data']['thirdPayNo'] ?? '';

        // 费率
        list($fee, $feeRate) = BasePay::instance($payChannel, $shopId, $sysType, $payeeConfigId)
                                      ->setChildPayChannel($payChannelChild)
                                      ->getFeeAmtByPayPrice($amount);

        PayStreamModel::addData($payNo, $shopId, $orderId, $payChannel, $payChannelChild,
            PayStreamModel::PAY_ACTION_APP, $amount, $fee, $feeRate, $sysType, $callbackUrl, 1, $subject, $bizId,
            $userId, $thirdPayNo, [], $payeeConfigId);

        $pay['data']['appPayData']['payNo'] = $payNo;
        return $pay['data']['appPayData'];
    }

    /**
     * 关闭支付
     * @param string $orderId
     * @param int $shopId
     * @param int $sysType
     * @return void
     * @throws PayException
     */
    public static function sClosePay(string $orderId, int $shopId, int $sysType): void
    {
        if (!in_array($sysType, [PayStreamModel::SYS_TYPE_ROS])) {
            throw new PayException(PayCodeEnum::ERR_REQUEST_PARAM, '目前仅支持订货关闭支付');
        }

        $payStreamModels = PayStreamModel::suffix(null, $sysType)
                                         ->where('orderId', $orderId)
                                         ->where('shopId', $shopId)
                                         ->where('sysType', $sysType)
                                         ->where('payStatus', PayStreamModel::PAY_STATUS_0)
                                         ->get();
        if (!$payStreamModels->count()) {
            throw new PayException(PayCodeEnum::ERR_APP_PREPAY_ERROR, '不存在未支付的记录');
        }

        $payStreamModels->each(function ($payStreamModel) use ($orderId, $shopId, $sysType) {

            Log::info("sClosePay start... {$orderId} {$shopId} {$sysType} {$payStreamModel->payNo}");
            BasePay::instance($payStreamModel->payChannel, $shopId, $sysType)->closePay($payStreamModel->payNo);
            Log::info("sClosePay end... {$orderId} {$shopId} {$sysType} {$payStreamModel->payNo}");
        });
    }

    /**
     * H5支付
     * @param int $shopId
     * @param string $subject
     * @param int $amount
     * @param string $orderId
     * @param int $payChannel
     * @param int $payChannelChild
     * @param int $sysType
     * @param string $callbackUrl
     * @param string $h5Url
     * @return mixed
     * @throws PayException
     */
    public static function sWapPay(
        int $shopId,
        string $subject,
        int $amount,
        string $orderId,
        int $payChannel,
        int $payChannelChild,
        int $sysType,
        string $callbackUrl,
        string $h5BackUrl,
        string $bizId
    ) {
        $pay = BasePay::instance($payChannel, $shopId, $sysType)
                      ->wapPay($subject, $amount, $h5BackUrl, $h5BackUrl);
        if (!$pay['status']) {
            throw new PayException(PayCodeEnum::ERR_APP_PREPAY_ERROR, $pay['msg'] ?? '');
        }

        $payNo = $pay['data']['payNo'];

        // 费率
        list($fee, $feeRate) = BasePay::instance($payChannel, $shopId, $sysType)
                                      ->setChildPayChannel($payChannelChild)
                                      ->getFeeAmtByPayPrice($amount);

        PayStreamModel::addData($payNo, $shopId, $orderId, $payChannel, $payChannelChild,
            PayStreamModel::PAY_ACTION_WAP, $amount, $fee, $feeRate, $sysType, $callbackUrl, 1, $subject, $bizId);

        return $pay['data'];
    }

    /**
     * native支付
     * @param int $shopId
     * @param string $subject
     * @param int $amount
     * @param string $orderId
     * @param int $payChannel
     * @param int $payChannelChild
     * @param int $sysType
     * @param string $bizId
     * @return mixed
     * @throws PayException
     */
    public static function sNativePay(
        int $shopId,
        string $subject,
        int $amount,
        string $orderId,
        int $payChannel,
        int $payChannelChild,
        int $sysType,
        string $bizId,
        int $expireTime,
        array $extra = []
    ) {
        $pay = BasePay::instance($payChannel, $shopId, $sysType)
                      ->nativePay($subject, $amount, $expireTime);
        if (!$pay['status']) {
            throw new PayException(PayCodeEnum::ERR_NATIVE_PREPAY_ERROR, $pay['msg'] ?? '');
        }

        $payNo = $pay['data']['payNo'];

        // 费率
        list($fee, $feeRate) = BasePay::instance($payChannel, $shopId, $sysType)
                                      ->setChildPayChannel($payChannelChild)
                                      ->getFeeAmtByPayPrice($amount);

        PayStreamModel::addData($payNo, $shopId, $orderId, $payChannel, $payChannelChild,
            PayStreamModel::PAY_ACTION_NATIVE, $amount, $fee, $feeRate, $sysType, '', 1, $subject, $bizId, 0, '', $extra);

        return $pay['data'];
    }

    /**
     * 异步支付成功后，要干的业务操作
     * @param PayStreamModel $payStream
     * @throws PayException
     * @throws CustomerCouponException
     * @throws OrderException
     */
    public static function businessAsynPaySuccess(PayStreamModel $payStream)
    {
        switch ($payStream->sysType) {
            case PayStreamModel::SYS_TYPE_RMS_MEAL:
                self::orderFinishedV2($payStream);
                break;
            case PayStreamModel::SYS_TYPE_RMS_COUPON_PACKAGE:
                self::couponPackageFinished($payStream);
                break;
        }

        // 己方的内部服务回调
        self::paySuccessCallback($payStream->callbackUrl, PayStreamModel::OPERATE_TYPE_PAY, $payStream->orderId,
            $payStream->payNo);
    }

    /**
     * 订单支付回调
     *
     * @param PayStreamModel $payStream
     * @throws OrderException
     * @throws PayException
     */
    public static function orderFinishedV2(PayStreamModel $payStream)
    {
        // 订单状态修改
        $serialNo = $payStream->payNo;
        $orderPay = PayService::sQuery($serialNo);

        $order = new OrderPay(0, $orderPay->shopId, $orderPay->orderId);

        // 判断 订单的支付状态
        $payMethod = OrderPay::switchFyChileChannel2OrderPayChannel($orderPay->payChannelChild);
        switch ($order->getOrderInfo()->orderStatus) {
            case CodeEnum::ORDER_STATUS_REVERSE:
                $order->setPayData([
                    'payMethod' => $payMethod,
                    'payEvent'  => CodeEnum::ORDER_PAY_EVENT_REFUND,
                ]);
                break;
            case CodeEnum::ORDER_STATUS_CANCEL:
                SendFs::newRobot(SendFs::ROBOT_BUSINESS_WARING)
                      ->setTitleContent('有人钻支付漏洞(订单取消后支付)', json_encode([
                          'shopId'  => $order->getOrderInfo()->shopId,
                          'orderId' => $order->getOrderInfo()->orderId
                      ]))->send();
            default:
                $order->setPayData([
                    'payMethod' => $payMethod,
                    'payEvent'  => CodeEnum::ORDER_PAY_EVENT_ORDER,
                    'payPrice'  => $orderPay->payPrice,
                ]);
        }

        $order->successV2();
        $order->finish();

    }

    /**
     * 券包购买成功回调
     *
     * @param $orderId
     * @param $payNo
     * @throws CustomerCouponException
     */
    public static function couponPackageFinished(PayStreamModel $payStream)
    {
        DB::beginTransaction();

        $orderId = $payStream->orderId;
        $payNo   = $payStream->payNo;

        // 券包订单更新支付状态
        CouponPackageOrderModel::paySuccess($orderId);

        $cp = CouponPackageOrderModel::query()->find($orderId);
        if ($cp) {

            // 增加券的销量
            CouponPackageModel::incrSales($cp->couponPackageId);
            DB::commit();

            // 发券
            CustomerCouponService::buyCouponPackageSendCoupon($cp->couponPackageId, $cp->userId,
                ActivityEnum::TYPE_COUPON_PACKAGE, [], $payStream->shopId, $orderId);
        } else {
            DB::commit();
            SendFs::newRobot()
                  ->setTitleContent('找不到券包购买记录', "orderId:{$orderId} payNo:{$payNo}")->send();
        }
    }
}
